Ordinary
About

만들면서 배우는 클린 아키텍처 (7 ~ 9)

profileordilov / 2022. 3. 3

아키텍처 요소 테스트하기

테스트 피라미드

테스트의 기본 전제는 만드는 비용이 적고, 유지보수하기 쉽고, 빨리 실행되고 안정적이어야 합니다. 단위와 단위를 넘는 테스트들은 만드는 비용이 비싸지고 설정에 따라 깨지기 쉬워집니다. 테스트가 다른 클래스에 의존한다면 의존되는 클래스들은 mock으로 대체합니다.

단위 테스트 다음은 통합 테스트로 인터페이스로 데이터를 보낸 뒤 제대로 동작하는지 확인합니다. 이때 단위를 넘는 테스트의 경우 어떤 시점에는 mock으로 대체해야 합니다.

단위 테스트로 도메인 엔티티 테스트하기

도메인 엔티티 테스트들은 대체로 다른 클래스에 의존하지 않기 때문에 단위 테스트로 충분합니다.

단위 테스트로 유즈케이스 테스트하기

유즈케이스도 단위 테스트를 하지만 모든 행동보다 특정 핵심만 집중해서 테스트하는 것이 좋습니다. 모든 동작을 검증하려고 하면 클래스가 조금이라도 바뀔 때마다 테스트를 변경해야 합니다.

통합 테스트로 웹 어댑터 테스트하기

실제 http 프로토콜로 테스트하기 보다 mockMvc 객체를 이용해 모킹하는 경우가 많습니다. 통합 테스트를 통해 유효한 입력에 따라 유즈 케이스의 호출과 응답을 제대로 하는지 확인할 수 있습니다. 실제로 네트워크로 연결되었을 때 처리되는 JSON 매핑, 요청 경로, 입력 검증등을 통합으로 테스트할 수 있습니다.

통합 테스트로 영속성 어댑터 테스트하기

@DataJpaTest로 데이터베이스에 실제로 접근해 제대로 매핑되는지 확인합니다. 인메모리 데이터베이스로 잘 돌아가더라도 실제 운영 데이터베이스와 다른 경우 에러가 발생하는 경우도 체크가 가능해집니다.

시스템 테스트로 주요 경로 테스트하기

@SpringBootTest로 실제로 네트워크를 띄워 HTTP 통신을 통해 API를 테스트합니다. 이때 서드파티 시스템의 경우 사용하기 어려운 경우 모킹이 필요할 수 있씁니다.

얼마만큼의 테스트가 충분할까?

라인 커버리지가 80%가 넘더라도 버그가 없을지 확신할 수 없습니다. 버그가 없는 걸 불가능하기 때문에 버그를 수정하고 이 과정에서 왜 생각하지 못했을지, 해결 방법을 생각해야 합니다. 그리고 이런 버그를 잡을 수 있는 테스트를 추가하면 됩니다.

경계 간 매핑하기

매핑하지 않기 전략

웹, 애플리케이션, 영속성 모두 같은 도메인 모델 클래스를 사용하는 경우입니다. 단순한 CRUD 유스케이스라면 매핑하지 않는 것도 괜찮습니다. 하지만 확장하게 된다면 웹, 어플리케이션, 영속성등 다양한 이유로 변경 이유가 생겨 단일 책임 원칙을 위반합니다.

양방향 매핑 전략

각 계층이 전용 모델을 갖고 있는 모델로 양방향 매핑 전략이라고 합니다. 모든 계층에서 모두 양방향으로 매핑하기 때문에 양방향 매핑으로 불립니다. 각 계층이 전용 모델을 갖고 있어 각 계층이 모델을 변경하더라도 다른 계층에는 영향이 없습니다. 단일 책임 원칙을 만족하는 장점이 있지만 너무 많은 보일러플레이트 코드가 생깁니다. 매핑 프레임워크를 쓰더라도 두 모델 간의 매핑을 구현하는데 시간이 들게 됩니다. 또한 도메인 모델이 계층 경계를 넘어서 통신하는데 사용되는 구조입니다. 도메인 객체를 입력 파라미터와 반환값으로 사용하고 있어 바깥쪽 계층 요구상항에 의해 변경될 위험이 있습니다.

완전 매핑 전략

각 연산마다 별도의 입출력 모델을 사용합니다. 입력에는 Command 처럼 입력에 특화된 모델을 따로 사용하는 구조입니다. 이런 매핑 전략은 여러 가지를 섞어쓸 수 있고 전역적으로 사용하지 않고 계층 사이에 명확한 구분할 때 좋습니다. 애플리케이션 계층과 영속성 계층 사이에서는 매핑 오버헤드 때문에 단점이 더 큽니다.

단방향 매핑 전략

각 계층의 모델들이 동일한 인터페이스를 구현합니다. 도메인 모델 객체를 바깥 계층에 전달하고 싶으면 매핑 없이 전달이 가능합니다. 인터페이스를 사용할지 전용 모델로 매핑할지를 바깥 계층에서 결정할 수 있습니다. 하지만 매핑이 계층마다 넘나들며 퍼져있어 개념적으로 어렵습니다. 이 전략은 계층 간의 모델이 비슷할 때 효과적입니다.

애플리케이션 조립하기

왜 조립까지 신경 써야 할까?

유즈케이스와 어댑터가 필요할 때 그냥 인스턴스화 하지 않는 이유는 의존성이 올바른 방향을 가리키게 하기 위해서입니다. 모든 의존성은 안쪽으로, 애플리케이션의 도메인 코드 방향으로 향해야 바깥 계층의 변경에서 안전합니다. 유즈케이스가 영속성 어댑터를 호출해야하고 스스로 인스턴스화 한다면 의존성은 잘못된 방향이 됩니다.

이렇게 객체 인스턴스를 생성할 책임은 모든 클래스에 대한 의존성을 가지는 설정 컴포넌트가 가져야 합니다. 설정 컴포넌트는 다음과 같은 역할을 수행해야 합니다.

  • 웹 어댑터 인스턴스 생성
  • HTTP 요청이 실제로 웹 어댑터로 전달되도록 보장
  • 유즈케이스 인스턴스 생성
  • 웹 어댑터에 유즈케이스 인스턴스 제공
  • 영속성 어댑터 인스턴스 생성
  • 유즈케이스에 영속성 어댑터 인스턴스 제공
  • 영속성 어댑터가 실제로 데이터베이스에 접근할수 있도록 보장

이렇게 많은 책임을 지고 있으면 단일 책임 원칙을 위반하는게 맞지만 나머지 부분들을 깔끔하게 만들어줍니다.

스프링의 클래스패스 스캐닝으로 조립하기

스프링에서 애플리케이션을 조립한 결과물을 애플리케이션 컨텍스트 라고 합니다. 애플리케이션 컨텍스트를 구성하는 객체를 이라고 합니다. 조립하는 방법에서 가장 인기있고 편리한 방법은 클래스패스 스캐닝입니다. @Component 애너테이션이 붙은 클래스를 찾아서 각 필드의 생성할 수 있는 객체를 생성합니다.

단점은 클래스패스 스캐닝 방식을 사용한다면 애너테이션을 사용한다는 점에서 프레임워크에 의존적입니다. 만약 라이브러리나 프레임워크를 만드는 입장에선 사용하지 말아야 할 방법입니다.

다른 단점은 문제점이 생겼을 때 스프링 내부에서 원인을 찾아야하기 때문에 부수효과가 생길 수 있습니다.

스프링 자바 컨피그로 조립하기

자바 컨피그는 애플리케이션 컨텍스트에 추가할 빈을 생성하는 설정 클래스를 만듭니다.